﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Resonance.TrainNode.Contracts.Entities;

namespace Resonance.TrainNode.Services
{
	internal static class SessionManager
	{
		private static IList<SessionInfo> Sessions = new List<SessionInfo>();

		#region Retrieve Session's Objects Functions

		private static SessionInfo GetSessionObj(string sessionID)
		{
			lock (Sessions)
			{
				return (from s in Sessions where s.Session.ID == sessionID select s).SingleOrDefault();
			}
		}

		private static JobInfo GetJobObj(string sessionID, string jobID)
		{
			lock (Sessions)
			{
				SessionInfo session = GetSessionObj(sessionID);
				if (session == null)
					return null;

				return (from j in session.Jobs where j.Job.ID == jobID select j).SingleOrDefault();
			}
		}

		private static NNet GetNNetObj(string sessionID, string NNetID)
		{
			lock (Sessions)
			{
				SessionInfo session = GetSessionObj(sessionID);
				return (from n in session.NNetworks where n.ID == NNetID select n).SingleOrDefault();
			}
		}

		#endregion

		#region Check Functions

		private static void CheckSessionValidity(string sessionID)
		{
			lock (Sessions)
			{
				SessionInfo session = GetSessionObj(sessionID);
				if (session == null)
				{
					throw new ArgumentException("SessionID not valid");
				}
			}
		}

		private static void CheckJobValidity(string sessionID, string jobID)
		{
			lock (Sessions)
			{
				JobInfo job = GetJobObj(sessionID, jobID);
				if (job == null)
				{
					throw new ArgumentException("JobID not valid");
				}
			}
		}

		private static void CheckNNetValidity(string sessionID, string NNetID)
		{
			lock (Sessions)
			{
				NNet net = GetNNetObj(sessionID, NNetID);
				if (net == null)
				{
					throw new ArgumentException("NNetwork not found");
				}
			}
		}

		private static void CheckSessionNotRunning(string sessionID)
		{
			lock (Sessions)
			{
				SessionInfo session = GetSessionObj(sessionID);
				if (session == null)
				{
					throw new ArgumentException("SessionID not valid");
				}
				if (session.Session.State == SessionStateEnum.Running ||
					session.Session.State == SessionStateEnum.Waiting ||
					session.Session.State == SessionStateEnum.Stopping)
				{
					throw new Exception("Session is executing");
				}
			}
		}

		private static void CheckJobNotRunning(string sessionID, string jobID)
		{
			lock (Sessions)
			{
				JobInfo job = GetJobObj(sessionID, jobID);
				if (job.Job.State == JobStateEnum.Running ||
					job.Job.State == JobStateEnum.Waiting ||
					job.Job.State == JobStateEnum.Stopping)
				{
					throw new Exception("Job is executing");
				}
			}
		}

		private static void CheckNNetNotUsed(string sessionID, string NNetID)
		{
			lock (Sessions)
			{
				CheckSessionValidity(sessionID);
				SessionInfo session = GetSessionObj(sessionID);
				if ((from j in session.Jobs where j.Network != null && j.Job.NNetID == NNetID select j).Any())
				{
					throw new Exception("NNet specified is used by a job");
				}
			}
		}

		#endregion

		#region Implementation Functions (thread safe, returns only copied objects)

		public static void AddPatterns(string sessionID, TrainPattern[] trainPatterns)
		{
			lock (Sessions)
			{
				CheckSessionValidity(sessionID);
				SessionInfo session = GetSessionObj(sessionID);

				CheckSessionNotRunning(sessionID);

				int patternCount = trainPatterns.Length;
				for (int k = 0; k < patternCount; k++)
				{
					// TODO: Check pattern compatibility with NNet
					session.TrainPatterns.Add(trainPatterns[k]);
				}
			}
		}

		public static void AddSession(string sessionID, string description)
		{
			lock (Sessions)
			{
				if (GetSessionObj(sessionID) != null)
				{
					throw new ArgumentException("SessionID already present");
				}
				SessionInfo newSession = new SessionInfo();
				newSession.TrainPatterns = new List<TrainPattern>();
				newSession.NNetworks = new List<NNet>();
				newSession.Jobs = new List<JobInfo>();
				TrainSession tSession = new TrainSession();
				newSession.Session = tSession;
				tSession.ID = sessionID;
				tSession.Description = description;
				tSession.State = SessionStateEnum.Initialized;
				tSession.TrainStartTime = null;
				tSession.TrainEndTime = null;
				Sessions.Add(newSession);
			}
		}

		public static void AddJob(string sessionID, string jobID, string NNetID, TrainAlgorithmTypeEnum trainAlgorithm, TrainParameters trainParams)
		{
			lock (Sessions)
			{
				CheckSessionValidity(sessionID);
				JobInfo checkJob = GetJobObj(sessionID, jobID);
				if (checkJob != null)
				{
					throw new ArgumentException("JobID already present");
				}
				CheckNNetValidity(sessionID, NNetID);
				SessionInfo session = GetSessionObj(sessionID);

				NNet net = GetNNetObj(sessionID, NNetID);
				JobInfo newJob = new JobInfo();
				newJob.Session = session;
				TrainJob tJob = new TrainJob();
				newJob.Job = tJob;
				tJob.ID = jobID;
				tJob.CurrentEpoch = 0;
				tJob.CurrentError = 0;
				tJob.NNetID = NNetID;

				tJob.TrainAlgorithmType = trainAlgorithm;
				tJob.TrainParams = trainParams;
				if (session.Session.State == SessionStateEnum.Running || session.Session.State == SessionStateEnum.Waiting)
				{
					tJob.State = JobStateEnum.Waiting;
				}
				else
				{
					tJob.State = JobStateEnum.Initialized;
				}

				session.Jobs.Add(newJob);
				ManageJobs();
			}
		}

		public static void RemoveJob(string sessionID, string jobID)
		{
			lock (Sessions)
			{
				CheckJobValidity(sessionID, jobID);
				CheckJobNotRunning(sessionID, jobID);
				SessionInfo session = GetSessionObj(sessionID);
				JobInfo job = GetJobObj(sessionID, jobID);
				session.Jobs.Remove(job);
			}
		}

		public static void AddNNet(string sessionID, NNet net)
		{
			lock (Sessions)
			{
				CheckSessionValidity(sessionID);
				SessionInfo session = GetSessionObj(sessionID);
				// TODO: check Network compatibility with patterns

				session.NNetworks.Add(net);
			}
		}

		public static void CreateNNet(string sessionID, ActivationFunctionEnum activationFunction, double sigmoidAlpha, string NNetID, int inputNeuronsCount, params int[] layersNeuronsCount)
		{
			lock (Sessions)
			{
				CheckSessionValidity(sessionID);
				SessionInfo session = GetSessionObj(sessionID);

				NNet chkNet = GetNNetObj(sessionID, NNetID);
				if (chkNet != null)
				{
					throw new ArgumentException("NNetID already present in session");
				}
				if (layersNeuronsCount == null || layersNeuronsCount.Length < 1)
				{
					throw new ArgumentException("The parameter layersNeuronCount is not valid");
				}
				int currLayer = 0;

				NNet net = new NNet();
				net.ID = NNetID;
				net.Layers = new NNetLayer[layersNeuronsCount.Length];
				net.ActivationFunction = activationFunction;
				net.SigmoidAlpha = sigmoidAlpha;
				net.InputCount = inputNeuronsCount;

				for (currLayer = 0; currLayer < layersNeuronsCount.Length; currLayer++)
				{
					NNetLayer newLayer = new NNetLayer();
					net.Layers[currLayer] = newLayer;
					newLayer.Neurons = new Neuron[layersNeuronsCount[currLayer]];
					int weightCount = currLayer == 0 ? inputNeuronsCount : layersNeuronsCount[currLayer - 1];
					for (int n = 0; n < newLayer.Neurons.Length; n++)
						newLayer.Neurons[n].Weights = new double[weightCount];
				}

				session.NNetworks.Add(net);
			}
		}

		public static NNet GetJobResult(string sessionID, string jobID)
		{
			lock (Sessions)
			{
				CheckSessionValidity(sessionID);
				CheckJobValidity(sessionID, jobID);
				JobInfo job = GetJobObj(sessionID, jobID);

				CheckJobNotRunning(sessionID, jobID);

				if (job.Job.State != JobStateEnum.Completed)
				{
					throw new Exception("Job has not been executed");
				}

				SessionInfo session = GetSessionObj(sessionID);
				AForge.Neuro.ActivationNetwork aNetwork = job.Network;
				// TODO: check id NNet is still valid
				NNet srcNet = GetNNetObj(sessionID, job.Job.NNetID);

				NNet net = new NNet();

				int currLayer = 0;

				net.ID = job.Job.NNetID + "_result";
				net.Layers = new NNetLayer[aNetwork.LayersCount];
				// TODO: manage multiple activation functions
				net.ActivationFunction = ActivationFunctionEnum.Sigmoid;
				net.SigmoidAlpha = srcNet.SigmoidAlpha;
				net.InputCount = srcNet.InputCount;

				for (currLayer = 0; currLayer < aNetwork.LayersCount; currLayer++)
				{
					NNetLayer newLayer = new NNetLayer();
					net.Layers[currLayer] = newLayer;
					newLayer.Neurons = new Neuron[aNetwork[currLayer].NeuronsCount];
					int weightCount = currLayer == 0 ? aNetwork.InputsCount : aNetwork[currLayer - 1].NeuronsCount;
					for (int n = 0; n < newLayer.Neurons.Length; n++)
					{
						newLayer.Neurons[n].Weights = new double[weightCount];
						newLayer.Neurons[n].Threshold = aNetwork[currLayer][n].Threshold;
						for (int w = 0; w < weightCount; w++)
						{
							newLayer.Neurons[n].Weights[w] = aNetwork[currLayer][n][w];
						}
					}
				}

				return net;
			}
		}

		public static IList<TrainJob> GetSessionJobs(string sessionID)
		{
			lock (Sessions)
			{
				CheckSessionValidity(sessionID);
				SessionInfo session = GetSessionObj(sessionID);
				List<TrainJob> jobs = new List<TrainJob>();
				foreach (JobInfo ji in session.Jobs)
				{
					jobs.Add(ji.Job.Clone());
				}
				return jobs;
			}
		}

		public static IList<TrainSession> GetSessionList()
		{
			lock (Sessions)
			{
				List<TrainSession> sessList = new List<TrainSession>();
				foreach (SessionInfo si in Sessions)
				{
					sessList.Add(si.Session.Clone());
				}
				return sessList;
			}
		}

		public static TrainSession GetSessionByID(string sessionID)
		{
			lock (Sessions)
			{
				CheckSessionValidity(sessionID);
				return GetSessionObj(sessionID).Session.Clone();
			}
		}

		public static void RemoveSession(string sessionID)
		{
			lock (Sessions)
			{
				CheckSessionValidity(sessionID);
				CheckSessionNotRunning(sessionID);
				SessionInfo session = GetSessionObj(sessionID);

				Sessions.Remove(session);
			}
		}

		public static void RemoveNNet(string sessionID, string NNetID)
		{
			lock (Sessions)
			{
				CheckSessionValidity(sessionID);
				SessionInfo session = GetSessionObj(sessionID);

				CheckNNetNotUsed(sessionID, NNetID);
				NNet net = GetNNetObj(sessionID, NNetID);
				session.NNetworks.Remove(net);
			}
		}

		public static void StartSession(string sessionID)
		{
			lock (Sessions)
			{
				CheckSessionValidity(sessionID);
				CheckSessionNotRunning(sessionID);
				SessionInfo session = GetSessionObj(sessionID);

				session.Session.State = SessionStateEnum.Waiting;
				session.Session.TrainStartTime = DateTime.Now;
				ManageJobs();
			}
		}

		public static void StopSession(string sessionID)
		{
			lock (Sessions)
			{
				CheckSessionValidity(sessionID);
				SessionInfo session = GetSessionObj(sessionID);

				if (session.Session.State == SessionStateEnum.Waiting || session.Session.State == SessionStateEnum.Running)
				{
					session.Session.State = SessionStateEnum.Stopping;
				}
				ManageJobs();
			}
		}

		#endregion

		#region Job Manging and Scheduling

		internal static void JobStateChangedCallback(JobInfo job)
		{
			lock (Sessions)
			{
				UpdateSessionState(job.Session);
				if (job.AssignedThread != null
					&& job.Job.State != JobStateEnum.Running
					&& job.Job.State != JobStateEnum.Stopping
					&& job.Job.State != JobStateEnum.Waiting)
				{
					job.AssignedThread = null;
				}
			}
		}

		private static void ManageJobs()
		{
			lock (Sessions)
			{
				foreach (SessionInfo session in Sessions)
				{
					UpdateSessionState(session);
				}
			}
		}

		private static void UpdateSessionState(SessionInfo session)
		{
			TrainSession tSession = session.Session;
			lock (Sessions)
			{
				// Updates job status in session
				switch (tSession.State)
				{
					case SessionStateEnum.Waiting:
					case SessionStateEnum.Running:
						foreach (JobInfo job in session.Jobs)
						{
							switch (job.Job.State)
							{
								case JobStateEnum.Initialized:
									job.Job.State = JobStateEnum.Waiting;
									break;
							}
						}
						break;
					case SessionStateEnum.Stopping:
						foreach (JobInfo job in session.Jobs)
						{
							switch (job.Job.State)
							{
								case JobStateEnum.Running:
								case JobStateEnum.Waiting:
									if (job.AssignedThread != null)
									{
										job.Job.State = JobStateEnum.Stopping;
									}
									else
									{
										job.Job.State = JobStateEnum.Stopped;
									}
									break;
							}
						}
						break;
				}

				// Updates session status
				// No jobs starting or executing
				if (!
							(from j in session.Jobs
							 where j.Job.State == JobStateEnum.Running ||
								   j.Job.State == JobStateEnum.Stopping ||
								   j.Job.State == JobStateEnum.Waiting
							 select j).Any()
							)
				{
					switch (tSession.State)
					{
						case SessionStateEnum.Running:
						case SessionStateEnum.Waiting:
							tSession.State = SessionStateEnum.Completed;
							tSession.TrainEndTime = DateTime.Now;
							break;
						case SessionStateEnum.Stopping:
							tSession.State = SessionStateEnum.Stopped;
							break;
					}
				}
				else if (
					(from j in session.Jobs
					 where j.Job.State == JobStateEnum.Running
					 select j).Any())
				{
					tSession.State = SessionStateEnum.Running;
				}

				StartWaitingJobs();
			}
		}

		private static void StartWaitingJobs()
		{
			// count jobs running in all sessions
			int jobsRunningCount = 0;
			foreach (SessionInfo currSession in (from s in Sessions where s.Session.State == SessionStateEnum.Running || s.Session.State == SessionStateEnum.Waiting select s))
			{
				jobsRunningCount += (from j in currSession.Jobs where j.Job.State == JobStateEnum.Running select j).Count();
			}

			if (jobsRunningCount < GlobalTrain.Node.CoreCount)
			{
				int jobStarted = 0;
				foreach (SessionInfo session in (from s in Sessions where s.Session.State == SessionStateEnum.Running || s.Session.State == SessionStateEnum.Waiting select s))
				{
					IEnumerable<JobInfo> jobsWaiting = (from j in session.Jobs where j.Job.State == JobStateEnum.Waiting select j);
					foreach (JobInfo job in jobsWaiting)
					{
						if ((jobStarted + jobsRunningCount) < GlobalTrain.Node.CoreCount)
						{
							jobStarted++;
							job.Job.State = JobStateEnum.Running;
							StartJobThread(job);
						}
						else
						{
							return;
						}
					}
				}
			}
		}

		private static void StartJobThread(JobInfo job)
		{
			Thread thread = new Thread(JobWorkerThread.MainLoop);
			thread.IsBackground = true;
			thread.Priority = ThreadPriority.BelowNormal;
			job.AssignedThread = thread;
			thread.Start(job);
		}

		#endregion

		#region Extension Methods for cloning objects

		private static TrainSession Clone(this TrainSession src)
		{
			TrainSession dest = new TrainSession();

			dest.Description = src.Description;
			dest.ID = src.ID;
			dest.State = src.State;
			dest.TrainEndTime = src.TrainEndTime;
			dest.TrainStartTime = src.TrainStartTime;

			return dest;
		}

		private static TrainParameters Clone(this TrainParameters src)
		{
			TrainParameters dest = new TrainParameters();

			dest.LearningRate = src.LearningRate;
			dest.MaxEpochs = src.MaxEpochs;
			dest.MaxError = src.MaxError;
			dest.Momentum = src.Momentum;

			return dest;
		}

		private static TrainJob Clone(this TrainJob src)
		{
			TrainJob dest = new TrainJob();

			dest.CurrentEpoch = src.CurrentEpoch;
			dest.CurrentError = src.CurrentError;
			dest.EndTime = src.EndTime;
			dest.ID = src.ID;
			dest.NNetID = src.NNetID;
			dest.StartTime = src.StartTime;
			dest.State = src.State;
			dest.TrainAlgorithmType = src.TrainAlgorithmType;
			dest.TrainParams = src.TrainParams.Clone();

			return dest;
		}

		#endregion

	}
}
